﻿#region Using directives

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Linq;

#endregion

namespace OpenComposite.Base.Collections
{
	[TypeConverter(typeof(ExpandableObjectConverter))]
	public class BindingListEx<T> : BindingList<T>
	{
		#region Initialization
		public BindingListEx()
			: base()
		{
		}
		public BindingListEx(IList<T> list)
			: base(list)
		{
		}
		#endregion

		#region Public Members

		#region Methods

		/// <summary>
		/// Adds an object to the end of the <see cref="T:OpenComposite.Base.Collections.BindingListEx`1"/>, if it not exists in the list.
		/// </summary>
		/// <param name="item">The object to be added to the end of the <see cref="T:OpenComposite.Base.Collections.BindingListEx`1"/>. The value can be null for reference types.</param>
		public void AddDistinct(T item)
		{
			if ( !this.Contains(item) ) this.Add(item);
		}
		/// <summary>
		/// Adds the elements of the specified collection to the end of the <see cref="T:OpenComposite.Base.Collections.BindingListEx`1"/>.
		/// </summary>
		/// <param name="collection">
		/// The collection whose elements should be added to the end of the <see cref="T:OpenComposite.Base.Collections.BindingListEx`1"/>. 
		/// The collection itself cannot be null, but it can contain elements that are null, if type T is a reference type.</param>
		/// <exception cref="T:System.ArgumentNullException">collection is null.</exception>
		public virtual void AddRange(IEnumerable<T> collection)
		{
			if ( collection == null ) throw new ArgumentNullException("collection");

			foreach ( var item in collection ) {
				base.Items.Add(item);
			}
			OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
		}
		/// <summary>
		/// Adds the elements of the specified collection to the end of the <see cref="T:OpenComposite.Base.Collections.BindingListEx`1"/>, if they are not exists in this list.
		/// </summary>
		/// <param name="collection">
		/// The collection whose elements should be added to the end of the <see cref="T:OpenComposite.Base.Collections.BindingListEx`1"/>. 
		/// The collection itself cannot be null, but it can contain elements that are null, if type T is a reference type.</param>
		/// <exception cref="T:System.ArgumentNullException">collection is null.</exception>
		public virtual void AddRangeDistinct(IEnumerable<T> collection)
		{
			if ( collection == null ) throw new ArgumentNullException("collection");

			foreach ( var item in collection ) {
				if ( !base.Items.Contains(item) ) base.Items.Add(item);
			}
			OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
		}

		/// <summary>
		/// Inserts an object to a specific index in the <see cref="T:OpenComposite.Base.Collections.BindingListEx`1"/>, if it not exists in the list.
		/// </summary>
		/// <param name="index">The zero-based index at which item should be inserted.</param>
		/// <param name="item">The object to insert.</param>
		public virtual void InsertDistinct(int index, T item)
		{
			if ( !this.Contains(item) ) this.Insert(index, item);
		}

		/// <summary>
		/// Copies the elements of the <see cref="T:OpenComposite.Base.Collections.BindingListEx`1"/> to a new array.
		/// </summary>
		/// <returns>
		/// An array containing copies of the elements of the <see cref="T:OpenComposite.Base.Collections.BindingListEx`1"/>.</returns>
		public T[] ToArray()
		{
			T[] array = new T[this.Count];
			this.CopyTo(array, 0);
			return array;
		}
		/// <summary>
		/// Determines whether the <see cref="T:OpenComposite.Base.Collections.BindingListEx`1"/> contains elements that match the conditions 
		/// defined by the specified predicate.
		/// </summary>
		/// <param name="match">
		/// The <see cref="T:System.Predicate`1"/> delegate that defines the conditions of the elements to search for.</param>
		/// <returns>true if the <see cref="T:OpenComposite.Base.Collections.BindingListEx`1"/> contains one or more elements that match the conditions 
		/// defined by the specified predicate; otherwise, false.</returns>
		/// <exception cref="T:System.ArgumentNullException">match is null.</exception>
		public bool Exists(Predicate<T> match)
		{
			if ( match == null ) throw new ArgumentNullException("match");

			for ( int i = 0; i < this.Count; i++ ) {
				if ( match(this.Items[i]) ) return true;
			}
			return false;
		}
		/// <summary>
		/// Searches for an element that matches the conditions defined by the specified predicate, 
		/// and returns the first occurrence within the entire <see cref="T:OpenComposite.Base.Collections.BindingListEx`1"/>.
		/// </summary>
		/// <param name="match">
		/// The <see cref="T:System.Predicate`1"/> delegate that defines the conditions of the element to search for.</param>
		/// <returns>The first element that matches the conditions defined by the specified predicate,
		/// if found; otherwise, the default value for type T.</returns>
		/// <exception cref="T:System.ArgumentNullException">match is null.</exception>
		public T Find(Predicate<T> match)
		{
			if ( match == null ) throw new ArgumentNullException("match");

			foreach ( var item in this ) {
				if ( match(item) ) return item;
			}
			return default(T);
		}
		/// <summary>
		/// Retrieves all the elements that match the conditions defined by the specified predicate.
		/// </summary>
		/// <param name="match">
		/// The <see cref="T:System.Predicate`1"/> delegate that defines the conditions of the elements to search for.</param>
		/// <returns>A <see cref="T:OpenComposite.Base.Collections.BindingListEx`1"/> containing all the elements that match the conditions defined by the specified predicate, 
		/// if found; otherwise, an empty <see cref="T:OpenComposite.Base.Collections.BindingListEx`1"/>.</returns>
		/// <exception cref="T:System.ArgumentNullException">match is null.</exception>
		public BindingListEx<T> FindAll(Predicate<T> match)
		{
			if ( match == null ) throw new ArgumentNullException("match");

			BindingListEx<T> result = new BindingListEx<T>();
			foreach ( var item in this ) {
				if ( match(item) ) result.Add(item);
			}
			return result;
		}
		/// <summary>
		/// Converts the elements in the current <see cref="T:OpenComposite.Base.Collections.BindingListEx`1"/> to another type, 
		/// and returns a list containing the converted elements.
		/// </summary>
		/// <typeparam name="TOutput">
		/// A <see cref="T:System.Converter`2"/> delegate that converts each element from one type to another type.</typeparam>
		/// <param name="converter"></param>
		/// <returns>
		/// A <see cref="T:OpenComposite.Base.Collections.BindingListEx`1"/> of the target type containing the converted elements 
		/// from the current <see cref="T:OpenComposite.Base.Collections.BindingListEx`1"/>.</returns>
		/// <exception cref="T:System.ArgumentNullException">converter is null.</exception>
		public BindingListEx<TOutput> ConvertAll<TOutput>(Converter<T, TOutput> converter)
		{
			if ( converter == null ) throw new ArgumentNullException("converter");

			BindingListEx<TOutput> output = new BindingListEx<TOutput>();
			foreach ( var item in this ) {
				output.Add(converter(item));
			}
			return output;
		}
		/// <summary>
		/// Returns a <see cref="T:System.String"/> that represents the values of current <see cref="T:OpenComposite.Base.Collections.BindingListEx"/> seperated by a given <see cref="T:System.Char"/>.
		/// </summary>
		/// <returns>
		/// A <see cref="T:System.String"/> that represents the values of current <see cref="T:OpenComposite.Base.Collections.BindingListEx"/> seperated by a given <see cref="T:System.Char"/>.
		/// </returns>
		public string ToString(char seperator)
		{
			List<string> list = new List<string>();
			foreach ( T item in this ) {
				if ( item == null )
					list.Add("");
				else
					list.Add(item.ToString());
			}
			return string.Join(seperator.ToString(), list.ToArray());
		}
		#endregion

		#endregion

		#region Sorting

		private bool _isSorted;
		private PropertyDescriptor _sortProperty;
		private ListSortDirection _sortDirection;

		/// <summary>
		/// Gets a value indicating whether the list supports sorting.
		/// </summary>
		/// <value></value>
		/// <returns>true if the list supports sorting; otherwise, false. The default is false.</returns>
		protected override bool SupportsSortingCore
		{
			get { return true; }
		}

		/// <summary>
		/// Gets the direction the list is sorted.
		/// </summary>
		/// <value></value>
		/// <returns>One of the <see cref="T:System.ComponentModel.ListSortDirection"/> values. The default is <see cref="F:System.ComponentModel.ListSortDirection.Ascending"/>. </returns>
		protected override ListSortDirection SortDirectionCore
		{
			get { return _sortDirection; }
		}

		/// <summary>
		/// Gets the property descriptor that is used for sorting the list if sorting is implemented in a derived class; otherwise, returns null.
		/// </summary>
		/// <value></value>
		/// <returns>The <see cref="T:System.ComponentModel.PropertyDescriptor"/> used for sorting the list.</returns>
		protected override PropertyDescriptor SortPropertyCore
		{
			get { return _sortProperty; }
		}

		/// <summary>
		/// Sorts the items if overridden in a derived class; otherwise, throws a <see cref="T:System.NotSupportedException"/>.
		/// </summary>
		/// <param name="prop">A <see cref="T:System.ComponentModel.PropertyDescriptor"/> that specifies the property to sort on.</param>
		/// <param name="direction">One of the <see cref="T:System.ComponentModel.ListSortDirection"/>  values.</param>
		/// <exception cref="T:System.NotSupportedException">Method is not overridden in a derived class. </exception>
		protected override void ApplySortCore(PropertyDescriptor prop, ListSortDirection direction)
		{

			// Get list to sort
			// Note: this.Items is a non-sortable ICollection<T>
			List<T> items = this.Items as List<T>;

			// Apply and set the sort, if items to sort
			if ( items != null ) {
				PropertyComparer<T> pc = new PropertyComparer<T>(prop, direction);
				items.Sort(pc);
				_isSorted = true;
			} else {
				_isSorted = false;
			}

			_sortProperty = prop;
			_sortDirection = direction;

			this.OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
		}

		/// <summary>
		/// Gets a value indicating whether the list is sorted.
		/// </summary>
		/// <value></value>
		/// <returns>true if the list is sorted; otherwise, false. The default is false.</returns>
		protected override bool IsSortedCore
		{
			get { return _isSorted; }
		}

		/// <summary>
		/// Removes any sort applied with <see cref="M:System.ComponentModel.BindingList`1.ApplySortCore(System.ComponentModel.PropertyDescriptor,System.ComponentModel.ListSortDirection)"/> if sorting is implemented in a derived class; otherwise, raises <see cref="T:System.NotSupportedException"/>.
		/// </summary>
		/// <exception cref="T:System.NotSupportedException">Method is not overridden in a derived class. </exception>
		protected override void RemoveSortCore()
		{
			_isSorted = false;
			this.OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
		}

		/// <summary>
		/// Sort the elements in the entire <see cref="T:OpenComposite.Base.Collections.BindingListEx`1"/>./>
		/// </summary>
		/// <param name="propertyName">The name of the property to sort on.</param>
		/// <param name="direction">One of the <see cref="T:System.ComponentModel.ListSortDirection"/> values.</param>
		public void Sort(string propertyName, ListSortDirection direction)
		{
			if ( propertyName == null ) throw new ArgumentNullException("propertyName");

			PropertyDescriptor propdesc = TypeDescriptor.GetProperties(typeof(T))[propertyName];
			Debug.Assert(propdesc != null,
				string.Format("The type {0} doesn't includes the property '{1}'.",
							  typeof(T).FullName, propertyName));

			if ( propdesc == null ) {
				this.RemoveSortCore();
			} else {
				this.ApplySortCore(propdesc, direction);
			}
		}
		#endregion

		#region Persistence

		// NOTE: BindingList<T> is not serializable but List<T> is
		/// <summary>
		/// Serialize the entire <see cref="T:OpenComposite.Base.Collections.BindingListEx`1"/> to a file.
		/// </summary>
		/// <param name="filename">The name of the target file.</param>
		public void Save(string filename)
		{
			BinaryFormatter formatter = new BinaryFormatter();
			using ( FileStream stream = new FileStream(filename, FileMode.Create) ) {
				// Serialize data list items
				formatter.Serialize(stream, (List<T>)this.Items);
			}
		}

		/// <summary>
		/// Deserialize a file to this <see cref="T:OpenComposite.Base.Collections.BindingListEx`1"/>.
		/// </summary>
		/// <param name="filename">The name of the source file.</param>
		public void Load(string filename)
		{

			this.ClearItems();

			if ( File.Exists(filename) ) {
				BinaryFormatter formatter = new BinaryFormatter();
				using ( FileStream stream = new FileStream(filename, FileMode.Open) ) {
					// Deserialize data list items
					( (List<T>)this.Items ).AddRange((IEnumerable<T>)formatter.Deserialize(stream));
				}
			}

			// Let bound controls know they should refresh their views
			this.OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
		}

		#endregion

		#region Filter
		//public Func<T, bool> Filter { get; set; }
		//protected new IList<T> Items
		//{
		//    get
		//    {
		//        if ( Filter == null ) return base.Items;
		//        IList<T> list = new List<T>();
		//        var items = from item in base.Items
		//                    where Filter.Invoke(item)
		//                    select item;
		//        this.AddRange(items);
		//        return list;
		//    }
		//}
		#endregion

		#region Searching

		/// <summary>
		/// Gets a value indicating whether the list supports searching.
		/// </summary>
		/// <value></value>
		/// <returns>true if the list supports searching; otherwise, false. The default is false.</returns>
		protected override bool SupportsSearchingCore
		{
			get { return true; }
		}

		/// <summary>
		/// Searches for the index of the item that has the specified property descriptor with the specified value, if searching is implemented in a derived class; otherwise, a <see cref="T:System.NotSupportedException"/>.
		/// </summary>
		/// <param name="prop">The <see cref="T:System.ComponentModel.PropertyDescriptor"/> to search for.</param>
		/// <param name="key">The value of <paramref name="property"/> to match.</param>
		/// <returns>
		/// The zero-based index of the item that matches the property descriptor and contains the specified value.
		/// </returns>
		/// <exception cref="T:System.NotSupportedException">
		/// 	<see cref="M:System.ComponentModel.BindingList`1.FindCore(System.ComponentModel.PropertyDescriptor,System.Object)"/> is not overridden in a derived class.</exception>
		protected override int FindCore(PropertyDescriptor prop, object key)
		{
			// Specify search columns
			if ( prop == null ) return -1;
			// Get list to search
			List<T> items = this.Items as List<T>;
			// Traverse list for value
			foreach ( T item in items ) {
				// Test column search value
				object value = prop.GetValue(item);
				// If value is the search value, return the index of the data item
				if ( ( key != null && key.Equals(value) ) || ( key == null && value == null ) ) {
					return IndexOf(item);
				}
			}
			return -1;
		}

		#endregion

	}
}